{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creating a Segmentation App Including Visualization with MONAI Deploy App SDK\n",
    "\n",
    "This tutorial shows how to create an organ segmentation application for a PyTorch model that has been trained with MONAI, and visualize the segmentation and input images with Clara Viz integration.\n",
    "\n",
    "When integrated with imaging networks and receiving DICOM instances from modalities and Picture Archiving and Communications System (PACS), an AI deploy application has to deal with a whole DICOM study with multiple series, whose images' spacing may not be the same as expected by the trained model. To address these cases consistently and efficiently, MONAI Deploy Application SDK provides classes, called operators, to parse DICOM studies, select specific series with application-defined rules, and convert the selected DICOM series into domain-specific image format along with meta-data representing the pertinent DICOM attributes. The image is then further processed in the pre-processing stage to normalize spacing, orientation, intensity,etc, before pixel data as Tensors are used for inference.\n",
    "\n",
    "During the development of an application, there is often times the need to visualize the transformed images between processing steps. Clara Viz is the perfect tool for this scenario, and a built-in operator in the App SDK makes the integration seamless and easy to implement, as we demonstrate below.\n",
    "\n",
    "\n",
    "## Creating Operators and connecting them in Application class\n",
    "\n",
    "We will implement an application that consists of five Operators:\n",
    "\n",
    "- **DICOMDataLoaderOperator**:\n",
    "    - **Input(dicom_files)**: a folder path ([`DataPath`](/modules/_autosummary/monai.deploy.core.domain.DataPath))\n",
    "    - **Output(dicom_study_list)**: a list of DICOM studies in memory (List[[`DICOMStudy`](/modules/_autosummary/monai.deploy.core.domain.DICOMStudy)])\n",
    "- **DICOMSeriesSelectorOperator**:\n",
    "    - **Input(dicom_study_list)**: a list of DICOM studies in memory (List[[`DICOMStudy`](/modules/_autosummary/monai.deploy.core.domain.DICOMStudy)])\n",
    "    - **Input(selection_rules)**: a selection rule (Dict)\n",
    "    - **Output(study_selected_series_list)**: a DICOM series object in memory ([`StudySelectedSeries`](/modules/_autosummary/monai.deploy.core.domain.StudySelectedSeries))\n",
    "- **DICOMSeriesToVolumeOperator**:\n",
    "    - **Input(study_selected_series_list)**: a DICOM series object in memory ([`StudySelectedSeries`](/modules/_autosummary/monai.deploy.core.domain.StudySelectedSeries))\n",
    "    - **Output(image)**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "- **MonaiBundleInferenceOperator**:\n",
    "    - **Input(image)**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "    - **Output(pred)**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "- **DICOMSegmentationWriterOperator**:\n",
    "    - **Input(seg_image)**: a segmentation image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "    - **Input(study_selected_series_list)**: a DICOM series object in memory ([`StudySelectedSeries`](/modules/_autosummary/monai.deploy.core.domain.StudySelectedSeries))\n",
    "    - **Output(dicom_seg_instance)**: a file path ([`DataPath`](/modules/_autosummary/monai.deploy.core.domain.DataPath))\n",
    "\n",
    "\n",
    ":::{note}\n",
    "The `DICOMSegmentationWriterOperator` needs both the segmentation image as well as the original DICOM series meta-data in order to use the patient demographics and the DICOM Study level attributes.\n",
    ":::\n",
    "\n",
    "The workflow of the application is illustrated below.\n",
    "\n",
    "```{mermaid}\n",
    "%%{init: {\"theme\": \"base\", \"themeVariables\": { \"fontSize\": \"16px\"}} }%%\n",
    "\n",
    "classDiagram\n",
    "    direction TB\n",
    "\n",
    "    DICOMDataLoaderOperator --|> DICOMSeriesSelectorOperator : dicom_study_list...dicom_study_list\n",
    "    DICOMSeriesSelectorOperator --|> DICOMSeriesToVolumeOperator : study_selected_series_list...study_selected_series_list\n",
    "    DICOMSeriesToVolumeOperator --|> MonaiBundleInferenceOperator : image...image\n",
    "    DICOMSeriesSelectorOperator --|> DICOMSegmentationWriterOperator : study_selected_series_list...study_selected_series_list\n",
    "    MonaiBundleInferenceOperator --|> DICOMSegmentationWriterOperator : pred...seg_image\n",
    "\n",
    "\n",
    "    class DICOMDataLoaderOperator {\n",
    "        <in>dicom_files : DISK\n",
    "        dicom_study_list(out) IN_MEMORY\n",
    "    }\n",
    "    class DICOMSeriesSelectorOperator {\n",
    "        <in>dicom_study_list : IN_MEMORY\n",
    "        <in>selection_rules : IN_MEMORY\n",
    "        study_selected_series_list(out) IN_MEMORY\n",
    "    }\n",
    "    class DICOMSeriesToVolumeOperator {\n",
    "        <in>study_selected_series_list : IN_MEMORY\n",
    "        image(out) IN_MEMORY\n",
    "    }\n",
    "    class MonaiBundleInferenceOperator {\n",
    "        <in>image : IN_MEMORY\n",
    "        pred(out) IN_MEMORY\n",
    "    }\n",
    "    class DICOMSegmentationWriterOperator {\n",
    "        <in>seg_image : IN_MEMORY\n",
    "        <in>study_selected_series_list : IN_MEMORY\n",
    "        dicom_seg_instance(out) DISK\n",
    "    }\n",
    "```\n",
    "\n",
    "### Setup environment\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install MONAI and other necessary image processing packages for the application\n",
    "!python -c \"import monai\" || pip install --upgrade -q \"monai\"\n",
    "!python -c \"import torch\" || pip install -q \"torch>=1.12.0\"\n",
    "!python -c \"import numpy\" || pip install -q \"numpy>=1.21.6\"\n",
    "!python -c \"import nibabel\" || pip install -q \"nibabel>=3.2.1\"\n",
    "!python -c \"import pydicom\" || pip install -q \"pydicom>=2.3.0\"\n",
    "!python -c \"import highdicom\" || pip install -q \"highdicom>=0.18.2\"\n",
    "!python -c \"import SimpleITK\" || pip install -q \"SimpleITK>=2.0.0\"\n",
    "\n",
    "# Install MONAI Deploy App SDK package\n",
    "!python -c \"import monai.deploy\" || pip install -q \"monai-deploy-app-sdk\"\n",
    "\n",
    "# Install Clara Viz package\n",
    "!python -c \"import clara.viz\" || pip install --upgrade -q \"clara-viz\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note: you may need to restart the Jupyter kernel to use the updated packages."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Download/Extract input and model/bundle files from Google Drive\n",
    "\n",
    "**_Note:_** Data files are now access controlled. Please first request permission to access the [shared folder on Google Drive](https://drive.google.com/drive/folders/1EONJsrwbGsS30td0hs8zl4WKjihew1Z3?usp=sharing). Please download zip file, `ai_spleen_seg_bundle_data.zip` in the `ai_spleen_seg_app` folder, to the same folder as the notebook example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Archive:  ai_spleen_seg_bundle_data.zip\n",
      "  inflating: dcm/1-001.dcm           \n",
      "  inflating: dcm/1-002.dcm           \n",
      "  inflating: dcm/1-003.dcm           \n",
      "  inflating: dcm/1-004.dcm           \n",
      "  inflating: dcm/1-005.dcm           \n",
      "  inflating: dcm/1-006.dcm           \n",
      "  inflating: dcm/1-007.dcm           \n",
      "  inflating: dcm/1-008.dcm           \n",
      "  inflating: dcm/1-009.dcm           \n",
      "  inflating: dcm/1-010.dcm           \n",
      "  inflating: dcm/1-011.dcm           \n",
      "  inflating: dcm/1-012.dcm           \n",
      "  inflating: dcm/1-013.dcm           \n",
      "  inflating: dcm/1-014.dcm           \n",
      "  inflating: dcm/1-015.dcm           \n",
      "  inflating: dcm/1-016.dcm           \n",
      "  inflating: dcm/1-017.dcm           \n",
      "  inflating: dcm/1-018.dcm           \n",
      "  inflating: dcm/1-019.dcm           \n",
      "  inflating: dcm/1-020.dcm           \n",
      "  inflating: dcm/1-021.dcm           \n",
      "  inflating: dcm/1-022.dcm           \n",
      "  inflating: dcm/1-023.dcm           \n",
      "  inflating: dcm/1-024.dcm           \n",
      "  inflating: dcm/1-025.dcm           \n",
      "  inflating: dcm/1-026.dcm           \n",
      "  inflating: dcm/1-027.dcm           \n",
      "  inflating: dcm/1-028.dcm           \n",
      "  inflating: dcm/1-029.dcm           \n",
      "  inflating: dcm/1-030.dcm           \n",
      "  inflating: dcm/1-031.dcm           \n",
      "  inflating: dcm/1-032.dcm           \n",
      "  inflating: dcm/1-033.dcm           \n",
      "  inflating: dcm/1-034.dcm           \n",
      "  inflating: dcm/1-035.dcm           \n",
      "  inflating: dcm/1-036.dcm           \n",
      "  inflating: dcm/1-037.dcm           \n",
      "  inflating: dcm/1-038.dcm           \n",
      "  inflating: dcm/1-039.dcm           \n",
      "  inflating: dcm/1-040.dcm           \n",
      "  inflating: dcm/1-041.dcm           \n",
      "  inflating: dcm/1-042.dcm           \n",
      "  inflating: dcm/1-043.dcm           \n",
      "  inflating: dcm/1-044.dcm           \n",
      "  inflating: dcm/1-045.dcm           \n",
      "  inflating: dcm/1-046.dcm           \n",
      "  inflating: dcm/1-047.dcm           \n",
      "  inflating: dcm/1-048.dcm           \n",
      "  inflating: dcm/1-049.dcm           \n",
      "  inflating: dcm/1-050.dcm           \n",
      "  inflating: dcm/1-051.dcm           \n",
      "  inflating: dcm/1-052.dcm           \n",
      "  inflating: dcm/1-053.dcm           \n",
      "  inflating: dcm/1-054.dcm           \n",
      "  inflating: dcm/1-055.dcm           \n",
      "  inflating: dcm/1-056.dcm           \n",
      "  inflating: dcm/1-057.dcm           \n",
      "  inflating: dcm/1-058.dcm           \n",
      "  inflating: dcm/1-059.dcm           \n",
      "  inflating: dcm/1-060.dcm           \n",
      "  inflating: dcm/1-061.dcm           \n",
      "  inflating: dcm/1-062.dcm           \n",
      "  inflating: dcm/1-063.dcm           \n",
      "  inflating: dcm/1-064.dcm           \n",
      "  inflating: dcm/1-065.dcm           \n",
      "  inflating: dcm/1-066.dcm           \n",
      "  inflating: dcm/1-067.dcm           \n",
      "  inflating: dcm/1-068.dcm           \n",
      "  inflating: dcm/1-069.dcm           \n",
      "  inflating: dcm/1-070.dcm           \n",
      "  inflating: dcm/1-071.dcm           \n",
      "  inflating: dcm/1-072.dcm           \n",
      "  inflating: dcm/1-073.dcm           \n",
      "  inflating: dcm/1-074.dcm           \n",
      "  inflating: dcm/1-075.dcm           \n",
      "  inflating: dcm/1-076.dcm           \n",
      "  inflating: dcm/1-077.dcm           \n",
      "  inflating: dcm/1-078.dcm           \n",
      "  inflating: dcm/1-079.dcm           \n",
      "  inflating: dcm/1-080.dcm           \n",
      "  inflating: dcm/1-081.dcm           \n",
      "  inflating: dcm/1-082.dcm           \n",
      "  inflating: dcm/1-083.dcm           \n",
      "  inflating: dcm/1-084.dcm           \n",
      "  inflating: dcm/1-085.dcm           \n",
      "  inflating: dcm/1-086.dcm           \n",
      "  inflating: dcm/1-087.dcm           \n",
      "  inflating: dcm/1-088.dcm           \n",
      "  inflating: dcm/1-089.dcm           \n",
      "  inflating: dcm/1-090.dcm           \n",
      "  inflating: dcm/1-091.dcm           \n",
      "  inflating: dcm/1-092.dcm           \n",
      "  inflating: dcm/1-093.dcm           \n",
      "  inflating: dcm/1-094.dcm           \n",
      "  inflating: dcm/1-095.dcm           \n",
      "  inflating: dcm/1-096.dcm           \n",
      "  inflating: dcm/1-097.dcm           \n",
      "  inflating: dcm/1-098.dcm           \n",
      "  inflating: dcm/1-099.dcm           \n",
      "  inflating: dcm/1-100.dcm           \n",
      "  inflating: dcm/1-101.dcm           \n",
      "  inflating: dcm/1-102.dcm           \n",
      "  inflating: dcm/1-103.dcm           \n",
      "  inflating: dcm/1-104.dcm           \n",
      "  inflating: dcm/1-105.dcm           \n",
      "  inflating: dcm/1-106.dcm           \n",
      "  inflating: dcm/1-107.dcm           \n",
      "  inflating: dcm/1-108.dcm           \n",
      "  inflating: dcm/1-109.dcm           \n",
      "  inflating: dcm/1-110.dcm           \n",
      "  inflating: dcm/1-111.dcm           \n",
      "  inflating: dcm/1-112.dcm           \n",
      "  inflating: dcm/1-113.dcm           \n",
      "  inflating: dcm/1-114.dcm           \n",
      "  inflating: dcm/1-115.dcm           \n",
      "  inflating: dcm/1-116.dcm           \n",
      "  inflating: dcm/1-117.dcm           \n",
      "  inflating: dcm/1-118.dcm           \n",
      "  inflating: dcm/1-119.dcm           \n",
      "  inflating: dcm/1-120.dcm           \n",
      "  inflating: dcm/1-121.dcm           \n",
      "  inflating: dcm/1-122.dcm           \n",
      "  inflating: dcm/1-123.dcm           \n",
      "  inflating: dcm/1-124.dcm           \n",
      "  inflating: dcm/1-125.dcm           \n",
      "  inflating: dcm/1-126.dcm           \n",
      "  inflating: dcm/1-127.dcm           \n",
      "  inflating: dcm/1-128.dcm           \n",
      "  inflating: dcm/1-129.dcm           \n",
      "  inflating: dcm/1-130.dcm           \n",
      "  inflating: dcm/1-131.dcm           \n",
      "  inflating: dcm/1-132.dcm           \n",
      "  inflating: dcm/1-133.dcm           \n",
      "  inflating: dcm/1-134.dcm           \n",
      "  inflating: dcm/1-135.dcm           \n",
      "  inflating: dcm/1-136.dcm           \n",
      "  inflating: dcm/1-137.dcm           \n",
      "  inflating: dcm/1-138.dcm           \n",
      "  inflating: dcm/1-139.dcm           \n",
      "  inflating: dcm/1-140.dcm           \n",
      "  inflating: dcm/1-141.dcm           \n",
      "  inflating: dcm/1-142.dcm           \n",
      "  inflating: dcm/1-143.dcm           \n",
      "  inflating: dcm/1-144.dcm           \n",
      "  inflating: dcm/1-145.dcm           \n",
      "  inflating: dcm/1-146.dcm           \n",
      "  inflating: dcm/1-147.dcm           \n",
      "  inflating: dcm/1-148.dcm           \n",
      "  inflating: dcm/1-149.dcm           \n",
      "  inflating: dcm/1-150.dcm           \n",
      "  inflating: dcm/1-151.dcm           \n",
      "  inflating: dcm/1-152.dcm           \n",
      "  inflating: dcm/1-153.dcm           \n",
      "  inflating: dcm/1-154.dcm           \n",
      "  inflating: dcm/1-155.dcm           \n",
      "  inflating: dcm/1-156.dcm           \n",
      "  inflating: dcm/1-157.dcm           \n",
      "  inflating: dcm/1-158.dcm           \n",
      "  inflating: dcm/1-159.dcm           \n",
      "  inflating: dcm/1-160.dcm           \n",
      "  inflating: dcm/1-161.dcm           \n",
      "  inflating: dcm/1-162.dcm           \n",
      "  inflating: dcm/1-163.dcm           \n",
      "  inflating: dcm/1-164.dcm           \n",
      "  inflating: dcm/1-165.dcm           \n",
      "  inflating: dcm/1-166.dcm           \n",
      "  inflating: dcm/1-167.dcm           \n",
      "  inflating: dcm/1-168.dcm           \n",
      "  inflating: dcm/1-169.dcm           \n",
      "  inflating: dcm/1-170.dcm           \n",
      "  inflating: dcm/1-171.dcm           \n",
      "  inflating: dcm/1-172.dcm           \n",
      "  inflating: dcm/1-173.dcm           \n",
      "  inflating: dcm/1-174.dcm           \n",
      "  inflating: dcm/1-175.dcm           \n",
      "  inflating: dcm/1-176.dcm           \n",
      "  inflating: dcm/1-177.dcm           \n",
      "  inflating: dcm/1-178.dcm           \n",
      "  inflating: dcm/1-179.dcm           \n",
      "  inflating: dcm/1-180.dcm           \n",
      "  inflating: dcm/1-181.dcm           \n",
      "  inflating: dcm/1-182.dcm           \n",
      "  inflating: dcm/1-183.dcm           \n",
      "  inflating: dcm/1-184.dcm           \n",
      "  inflating: dcm/1-185.dcm           \n",
      "  inflating: dcm/1-186.dcm           \n",
      "  inflating: dcm/1-187.dcm           \n",
      "  inflating: dcm/1-188.dcm           \n",
      "  inflating: dcm/1-189.dcm           \n",
      "  inflating: dcm/1-190.dcm           \n",
      "  inflating: dcm/1-191.dcm           \n",
      "  inflating: dcm/1-192.dcm           \n",
      "  inflating: dcm/1-193.dcm           \n",
      "  inflating: dcm/1-194.dcm           \n",
      "  inflating: dcm/1-195.dcm           \n",
      "  inflating: dcm/1-196.dcm           \n",
      "  inflating: dcm/1-197.dcm           \n",
      "  inflating: dcm/1-198.dcm           \n",
      "  inflating: dcm/1-199.dcm           \n",
      "  inflating: dcm/1-200.dcm           \n",
      "  inflating: dcm/1-201.dcm           \n",
      "  inflating: dcm/1-202.dcm           \n",
      "  inflating: dcm/1-203.dcm           \n",
      "  inflating: dcm/1-204.dcm           \n",
      "  inflating: model.ts                \n",
      "model.ts\n"
     ]
    }
   ],
   "source": [
    "# Download ai_spleen_bundle_data test data zip file. Please request access and download manually.\n",
    "# !pip install gdown\n",
    "# !gdown \"https://drive.google.com/uc?id=1IwWMpbo2fd38fKIqeIdL8SKTGvkn31tK\"\n",
    "\n",
    "# Clean up the destination folder for the input DICOM files\n",
    "!rm -rf dcm\n",
    "\n",
    "# After downloading ai_spleen_bundle_data zip file from the web browser or using gdown,\n",
    "!unzip -o \"ai_spleen_seg_bundle_data.zip\"\n",
    "\n",
    "# Need to copy the model.ts file to its own clean subfolder for packaging, to workaround an issue in the Packager\n",
    "models_folder = \"models\"\n",
    "!rm -rf {models_folder} && mkdir -p {models_folder}/model && cp model.ts {models_folder}/model && ls {models_folder}/model"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set up environment variables"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "env: HOLOSCAN_INPUT_PATH=dcm\n",
      "env: HOLOSCAN_MODEL_PATH=models\n",
      "env: HOLOSCAN_OUTPUT_PATH=output\n",
      "\u001b[0m\u001b[01;34mmodel\u001b[0m/\n"
     ]
    }
   ],
   "source": [
    "%env HOLOSCAN_INPUT_PATH dcm\n",
    "%env HOLOSCAN_MODEL_PATH {models_folder}\n",
    "%env HOLOSCAN_OUTPUT_PATH output\n",
    "%ls $HOLOSCAN_MODEL_PATH"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set up imports\n",
    "\n",
    "Let's import necessary classes/decorators to define Application and Operator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "import logging\n",
    "from pathlib import Path\n",
    "\n",
    "# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.\n",
    "from pydicom.sr.codedict import codes\n",
    "\n",
    "from monai.deploy.conditions import CountCondition\n",
    "from monai.deploy.core import AppContext, Application\n",
    "from monai.deploy.core.domain import Image\n",
    "from monai.deploy.core.io_type import IOType\n",
    "from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator\n",
    "from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription\n",
    "from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator\n",
    "from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator\n",
    "from monai.deploy.operators.monai_bundle_inference_operator import (\n",
    "    BundleConfigNames,\n",
    "    IOMapping,\n",
    "    MonaiBundleInferenceOperator,\n",
    ")\n",
    "from monai.deploy.operators.stl_conversion_operator import STLConversionOperator\n",
    "from monai.deploy.operators.clara_viz_operator import ClaraVizOperator"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Determining the Input and Output for the Model Bundle Inference Operator\n",
    "\n",
    "The App SDK provides a `MonaiBundleInferenceOperator` class to perform inference with a MONAI Bundle, which is essentially a PyTorch model in TorchScript with additional metadata describing the model network and processing specification. This operator uses the MONAI utilities to parse a MONAI Bundle to automatically instantiate the objects required for input and output processing as well as inference, as such it depends on MONAI transforms, inferers, and in turn their dependencies.\n",
    "\n",
    "Each Operator class inherits from the base [Operator](/modules/_autosummary/monai.deploy.core.Operator) class, and its input/output properties are specified by using [@input](/modules/_autosummary/monai.deploy.core.input)/[@output](/modules/_autosummary/monai.deploy.core.output) decorators. For the `MonaiBundleInferenceOperator` class, the input/output need to be defined to match those of the model network, both in name and data type. For the current release, an `IOMapping` object is used to connect the operator input/output to those of the model network by using the same names. This is likely to change, to be automated, in the future releases once certain limitation in the App SDK is removed.\n",
    "\n",
    "The Spleen CT Segmentation model network has a named input, called \"image\", and the named output called \"pred\", and both are of image type, which can all be mapped to the App SDK [Image](/modules/_autosummary/monai.deploy.core.domain.Image). This piece of information is typically acquired by examining the model metadata `network_data_format` attribute in the bundle, as seen in this [example] (https://github.com/Project-MONAI/model-zoo/blob/dev/models/spleen_ct_segmentation/configs/metadata.json)."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating Application class\n",
    "\n",
    "Our application class would look like below.\n",
    "\n",
    "It defines `App` class, inheriting [Application](/modules/_autosummary/monai.deploy.core.Application) class.\n",
    "\n",
    "The requirements (resource and package dependency) for the App can be specified by using [@resource](/modules/_autosummary/monai.deploy.core.resource) and [@env](/modules/_autosummary/monai.deploy.core.env) decorators.\n",
    "\n",
    "Objects required for DICOM parsing, series selection, pixel data conversion to volume image, model specific inference, and the AI result specific DICOM Segmentation object writers are created. The execution pipeline, as a Directed Acyclic Graph, is then constructed by connecting these objects through <a href=\"../../modules/_autosummary/monai.deploy.core.Application.html#monai.deploy.core.Application.add_flow\">self.add_flow()</a>."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class AISpleenSegApp(Application):\n",
    "    \"\"\"Demonstrates inference with built-in MONAI Bundle inference operator with DICOM files as input/output\n",
    "\n",
    "    This application loads a set of DICOM instances, select the appropriate series, converts the series to\n",
    "    3D volume image, performs inference with the built-in MONAI Bundle inference operator, including pre-processing\n",
    "    and post-processing, save the segmentation image in a DICOM Seg OID in an instance file, and optionally the\n",
    "    surface mesh in STL format.\n",
    "\n",
    "    Pertinent MONAI Bundle:\n",
    "      https://github.com/Project-MONAI/model-zoo/tree/dev/models/spleen_ct_segmentation\n",
    "\n",
    "    Execution Time Estimate:\n",
    "      With a Nvidia GV100 32GB GPU, for an input DICOM Series of 515 instances, the execution time is around\n",
    "      25 seconds with saving both DICOM Seg and surface mesh STL file, and 15 seconds with DICOM Seg only.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        \"\"\"Creates an application instance.\"\"\"\n",
    "        self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "    def run(self, *args, **kwargs):\n",
    "        # This method calls the base class to run. Can be omitted if simply calling through.\n",
    "        self._logger.info(f\"Begin {self.run.__name__}\")\n",
    "        super().run(*args, **kwargs)\n",
    "        self._logger.info(f\"End {self.run.__name__}\")\n",
    "\n",
    "    def compose(self):\n",
    "        \"\"\"Creates the app specific operators and chain them up in the processing DAG.\"\"\"\n",
    "\n",
    "        logging.info(f\"Begin {self.compose.__name__}\")\n",
    "\n",
    "        app_context = Application.init_app_context({})  # Do not pass argv in Jupyter Notebook\n",
    "        app_input_path = Path(app_context.input_path)\n",
    "        app_output_path = Path(app_context.output_path)\n",
    "        model_path = Path(app_context.model_path)\n",
    "\n",
    "        # Create the custom operator(s) as well as SDK built-in operator(s).\n",
    "        study_loader_op = DICOMDataLoaderOperator(\n",
    "            self, CountCondition(self, 1), input_folder=app_input_path, name=\"study_loader_op\"\n",
    "        )\n",
    "        series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name=\"series_selector_op\")\n",
    "        series_to_vol_op = DICOMSeriesToVolumeOperator(self, name=\"series_to_vol_op\")\n",
    "\n",
    "        # Create the inference operator that supports MONAI Bundle and automates the inference.\n",
    "        # The IOMapping labels match the input and prediction keys in the pre and post processing.\n",
    "        # The model_name is optional when the app has only one model.\n",
    "        # The bundle_path argument optionally can be set to an accessible bundle file path in the dev\n",
    "        # environment, so when the app is packaged into a MAP, the operator can complete the bundle parsing\n",
    "        # during init.\n",
    "\n",
    "        config_names = BundleConfigNames(config_names=[\"inference\"])  # Same as the default\n",
    "\n",
    "        bundle_spleen_seg_op = MonaiBundleInferenceOperator(\n",
    "            self,\n",
    "            input_mapping=[IOMapping(\"image\", Image, IOType.IN_MEMORY)],\n",
    "            output_mapping=[IOMapping(\"pred\", Image, IOType.IN_MEMORY)],\n",
    "            app_context=app_context,\n",
    "            bundle_config_names=config_names,\n",
    "            bundle_path=model_path,\n",
    "            name=\"bundle_spleen_seg_op\",\n",
    "        )\n",
    "\n",
    "        # Create DICOM Seg writer providing the required segment description for each segment with\n",
    "        # the actual algorithm and the pertinent organ/tissue. The segment_label, algorithm_name,\n",
    "        # and algorithm_version are of DICOM VR LO type, limited to 64 chars.\n",
    "        # https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html\n",
    "        segment_descriptions = [\n",
    "            SegmentDescription(\n",
    "                segment_label=\"Spleen\",\n",
    "                segmented_property_category=codes.SCT.Organ,\n",
    "                segmented_property_type=codes.SCT.Spleen,\n",
    "                algorithm_name=\"volumetric (3D) segmentation of the spleen from CT image\",\n",
    "                algorithm_family=codes.DCM.ArtificialIntelligence,\n",
    "                algorithm_version=\"0.3.2\",\n",
    "            )\n",
    "        ]\n",
    "\n",
    "        custom_tags = {\"SeriesDescription\": \"AI generated Seg, not for clinical use.\"}\n",
    "\n",
    "        dicom_seg_writer = DICOMSegmentationWriterOperator(\n",
    "            self,\n",
    "            segment_descriptions=segment_descriptions,\n",
    "            custom_tags=custom_tags,\n",
    "            output_folder=app_output_path,\n",
    "            name=\"dicom_seg_writer\",\n",
    "        )\n",
    "\n",
    "        # Create the processing pipeline, by specifying the source and destination operators, and\n",
    "        # ensuring the output from the former matches the input of the latter, in both name and type.\n",
    "        self.add_flow(study_loader_op, series_selector_op, {(\"dicom_study_list\", \"dicom_study_list\")})\n",
    "        self.add_flow(\n",
    "            series_selector_op, series_to_vol_op, {(\"study_selected_series_list\", \"study_selected_series_list\")}\n",
    "        )\n",
    "        self.add_flow(series_to_vol_op, bundle_spleen_seg_op, {(\"image\", \"image\")})\n",
    "        # Note below the dicom_seg_writer requires two inputs, each coming from a source operator.\n",
    "        self.add_flow(\n",
    "            series_selector_op, dicom_seg_writer, {(\"study_selected_series_list\", \"study_selected_series_list\")}\n",
    "        )\n",
    "        self.add_flow(bundle_spleen_seg_op, dicom_seg_writer, {(\"pred\", \"seg_image\")})\n",
    "        # Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by\n",
    "        # uncommenting the following couple lines.\n",
    "        stl_conversion_op = STLConversionOperator(\n",
    "            self, output_file=app_output_path.joinpath(\"stl/spleen.stl\"), name=\"stl_conversion_op\"\n",
    "        )\n",
    "        self.add_flow(bundle_spleen_seg_op, stl_conversion_op, {(\"pred\", \"image\")})\n",
    "\n",
    "        # Create the ClaraViz operator and feed both the seg and input Image object to it\n",
    "        viz_op = ClaraVizOperator(self, name=\"clara_viz_op\")\n",
    "        self.add_flow(series_to_vol_op, viz_op, {(\"image\", \"image\")})\n",
    "        self.add_flow(bundle_spleen_seg_op, viz_op, {(\"pred\", \"seg_image\")})\n",
    "\n",
    "        logging.info(f\"End {self.compose.__name__}\")\n",
    "\n",
    "\n",
    "# This is a sample series selection rule in JSON, simply selecting CT series.\n",
    "# If the study has more than 1 CT series, then all of them will be selected.\n",
    "# Please see more detail in DICOMSeriesSelectorOperator.\n",
    "Sample_Rules_Text = \"\"\"\n",
    "{\n",
    "    \"selections\": [\n",
    "        {\n",
    "            \"name\": \"CT Series\",\n",
    "            \"conditions\": {\n",
    "                \"StudyDescription\": \"(.*?)\",\n",
    "                \"Modality\": \"(?i)CT\",\n",
    "                \"SeriesDescription\": \"(.*?)\"\n",
    "            }\n",
    "        }\n",
    "    ]\n",
    "}\n",
    "\"\"\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Executing app locally\n",
    "\n",
    "We can execute the app in the Jupyter notebook. Note that the DICOM files of the CT Abdomen series must be present in the `dcm` and the Torch Script model at `model.ts`. Please use the actual path in your environment.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[info] [fragment.cpp:599] Loading extensions from configs...\n",
      "[2025-01-29 14:41:46,387] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, argv=[])\n",
      "[2025-01-29 14:41:46,393] [INFO] (root) - AppContext object: AppContext(input_path=dcm, output_path=output, model_path=models, workdir=)\n",
      "[2025-01-29 14:41:46,398] [INFO] (root) - End compose\n",
      "[info] [gxf_executor.cpp:264] Creating context\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'input_folder'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'dicom_study_list'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'seg_image'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'output_file'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'output_folder'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n",
      "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'seg_image'\n",
      "[info] [gxf_executor.cpp:2208] Activating Graph...\n",
      "[info] [gxf_executor.cpp:2238] Running Graph...\n",
      "[info] [gxf_executor.cpp:2240] Waiting for completion...\n",
      "[info] [greedy_scheduler.cpp:191] Scheduling 7 entities\n",
      "[2025-01-29 14:41:46,525] [INFO] (monai.deploy.operators.dicom_data_loader_operator.DICOMDataLoaderOperator) - No or invalid input path from the optional input port: None\n",
      "[2025-01-29 14:41:46,884] [INFO] (root) - Finding series for Selection named: CT Series\n",
      "[2025-01-29 14:41:46,885] [INFO] (root) - Searching study, : 1.3.6.1.4.1.14519.5.2.1.7085.2626.822645453932810382886582736291\n",
      "  # of series: 1\n",
      "[2025-01-29 14:41:46,886] [INFO] (root) - Working on series, instance UID: 1.3.6.1.4.1.14519.5.2.1.7085.2626.119403521930927333027265674239\n",
      "[2025-01-29 14:41:46,887] [INFO] (root) - On attribute: 'StudyDescription' to match value: '(.*?)'\n",
      "[2025-01-29 14:41:46,888] [INFO] (root) -     Series attribute StudyDescription value: CT ABDOMEN W IV CONTRAST\n",
      "[2025-01-29 14:41:46,889] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:41:46,890] [INFO] (root) - On attribute: 'Modality' to match value: '(?i)CT'\n",
      "[2025-01-29 14:41:46,891] [INFO] (root) -     Series attribute Modality value: CT\n",
      "[2025-01-29 14:41:46,892] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:41:46,893] [INFO] (root) - On attribute: 'SeriesDescription' to match value: '(.*?)'\n",
      "[2025-01-29 14:41:46,894] [INFO] (root) -     Series attribute SeriesDescription value: ABD/PANC 3.0 B31f\n",
      "[2025-01-29 14:41:46,895] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:41:46,897] [INFO] (root) - Selected Series, UID: 1.3.6.1.4.1.14519.5.2.1.7085.2626.119403521930927333027265674239\n",
      "[2025-01-29 14:41:47,361] [INFO] (root) - Parsing from bundle_path: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models/model/model.ts\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/bundle/reference_resolver.py:216: UserWarning: Detected deprecated name 'optional_packages_version' in configuration file, replacing with 'required_packages_version'.\n",
      "  warnings.warn(\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "26dacc25a4e94b69ab54eca02db67039",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Box(children=(Widget(), VBox(children=(interactive(children=(Dropdown(description='View mode', index=2, option…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[2025-01-29 14:41:58,112] [INFO] (monai.deploy.operators.stl_conversion_operator.STLConversionOperator) - Output will be saved in file output/stl/spleen.stl.\n",
      "[2025-01-29 14:42:00,510] [INFO] (monai.deploy.operators.stl_conversion_operator.SpatialImage) - 3D image\n",
      "[2025-01-29 14:42:00,512] [INFO] (monai.deploy.operators.stl_conversion_operator.STLConverter) - Image ndarray shape:(204, 512, 512)\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/highdicom/base.py:163: UserWarning: The string \"C3N-00198\" is unlikely to represent the intended person name since it contains only a single component. Construct a person name according to the format in described in https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#sect_6.2.1.2, or, in pydicom 2.2.0 or later, use the pydicom.valuerep.PersonName.from_named_components() method to construct the person name correctly. If a single-component name is really intended, add a trailing caret character to disambiguate the name.\n",
      "  check_person_name(patient_name)\n",
      "[2025-01-29 14:42:15,314] [INFO] (highdicom.base) - copy Image-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:42:15,315] [INFO] (highdicom.base) - copy attributes of module \"Specimen\"\n",
      "[2025-01-29 14:42:15,316] [INFO] (highdicom.base) - copy Patient-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:42:15,317] [INFO] (highdicom.base) - copy attributes of module \"Patient\"\n",
      "[2025-01-29 14:42:15,319] [INFO] (highdicom.base) - copy attributes of module \"Clinical Trial Subject\"\n",
      "[2025-01-29 14:42:15,320] [INFO] (highdicom.base) - copy Study-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:42:15,322] [INFO] (highdicom.base) - copy attributes of module \"General Study\"\n",
      "[2025-01-29 14:42:15,323] [INFO] (highdicom.base) - copy attributes of module \"Patient Study\"\n",
      "[2025-01-29 14:42:15,324] [INFO] (highdicom.base) - copy attributes of module \"Clinical Trial Study\"\n",
      "[info] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n",
      "[info] [greedy_scheduler.cpp:401] Scheduler finished.\n",
      "[info] [gxf_executor.cpp:2243] Deactivating Graph...\n",
      "[info] [gxf_executor.cpp:2251] Graph execution finished.\n",
      "[2025-01-29 14:42:15,492] [INFO] (__main__.AISpleenSegApp) - End run\n"
     ]
    }
   ],
   "source": [
    "!rm -rf $HOLOSCAN_OUTPUT_PATH\n",
    "AISpleenSegApp().run()\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once the application is verified inside Jupyter notebook, we can write the above Python code into Python files in an application folder.\n",
    "\n",
    "The application folder structure would look like below:\n",
    "\n",
    "```bash\n",
    "my_app\n",
    "├── __main__.py\n",
    "└── app.py\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create an application folder\n",
    "!mkdir -p my_app && rm -rf my_app/*"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### app.py"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing my_app/app.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile my_app/app.py\n",
    "\n",
    "# Copyright 2021-2023 MONAI Consortium\n",
    "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#     http://www.apache.org/licenses/LICENSE-2.0\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License.\n",
    "\n",
    "\n",
    "import logging\n",
    "from pathlib import Path\n",
    "\n",
    "# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.\n",
    "from pydicom.sr.codedict import codes\n",
    "\n",
    "from monai.deploy.conditions import CountCondition\n",
    "from monai.deploy.core import AppContext, Application\n",
    "from monai.deploy.core.domain import Image\n",
    "from monai.deploy.core.io_type import IOType\n",
    "from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator\n",
    "from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription\n",
    "from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator\n",
    "from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator\n",
    "from monai.deploy.operators.monai_bundle_inference_operator import (\n",
    "    BundleConfigNames,\n",
    "    IOMapping,\n",
    "    MonaiBundleInferenceOperator,\n",
    ")\n",
    "from monai.deploy.operators.stl_conversion_operator import STLConversionOperator\n",
    "from monai.deploy.operators.clara_viz_operator import ClaraVizOperator\n",
    "\n",
    "\n",
    "class AISpleenSegApp(Application):\n",
    "    \"\"\"Demonstrates inference with built-in MONAI Bundle inference operator with DICOM files as input/output\n",
    "\n",
    "    This application loads a set of DICOM instances, select the appropriate series, converts the series to\n",
    "    3D volume image, performs inference with the built-in MONAI Bundle inference operator, including pre-processing\n",
    "    and post-processing, save the segmentation image in a DICOM Seg OID in an instance file, and optionally the\n",
    "    surface mesh in STL format.\n",
    "\n",
    "    Pertinent MONAI Bundle:\n",
    "      https://github.com/Project-MONAI/model-zoo/tree/dev/models/spleen_ct_segmentation\n",
    "\n",
    "    Execution Time Estimate:\n",
    "      With a Nvidia GV100 32GB GPU, for an input DICOM Series of 515 instances, the execution time is around\n",
    "      25 seconds with saving both DICOM Seg and surface mesh STL file, and 15 seconds with DICOM Seg only.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        \"\"\"Creates an application instance.\"\"\"\n",
    "        self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "    def run(self, *args, **kwargs):\n",
    "        # This method calls the base class to run. Can be omitted if simply calling through.\n",
    "        self._logger.info(f\"Begin {self.run.__name__}\")\n",
    "        super().run(*args, **kwargs)\n",
    "        self._logger.info(f\"End {self.run.__name__}\")\n",
    "\n",
    "    def compose(self):\n",
    "        \"\"\"Creates the app specific operators and chain them up in the processing DAG.\"\"\"\n",
    "\n",
    "        logging.info(f\"Begin {self.compose.__name__}\")\n",
    "\n",
    "        # Use command line options over environment variables to init context.\n",
    "        app_context = Application.init_app_context(self.argv)\n",
    "        app_input_path = Path(app_context.input_path)\n",
    "        app_output_path = Path(app_context.output_path)\n",
    "        model_path = Path(app_context.model_path)\n",
    "\n",
    "        # Create the custom operator(s) as well as SDK built-in operator(s).\n",
    "        study_loader_op = DICOMDataLoaderOperator(\n",
    "            self, CountCondition(self, 1), input_folder=app_input_path, name=\"study_loader_op\"\n",
    "        )\n",
    "        series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name=\"series_selector_op\")\n",
    "        series_to_vol_op = DICOMSeriesToVolumeOperator(self, name=\"series_to_vol_op\")\n",
    "\n",
    "        # Create the inference operator that supports MONAI Bundle and automates the inference.\n",
    "        # The IOMapping labels match the input and prediction keys in the pre and post processing.\n",
    "        # The model_name is optional when the app has only one model.\n",
    "        # The bundle_path argument optionally can be set to an accessible bundle file path in the dev\n",
    "        # environment, so when the app is packaged into a MAP, the operator can complete the bundle parsing\n",
    "        # during init.\n",
    "\n",
    "        config_names = BundleConfigNames(config_names=[\"inference\"])  # Same as the default\n",
    "\n",
    "        bundle_spleen_seg_op = MonaiBundleInferenceOperator(\n",
    "            self,\n",
    "            input_mapping=[IOMapping(\"image\", Image, IOType.IN_MEMORY)],\n",
    "            output_mapping=[IOMapping(\"pred\", Image, IOType.IN_MEMORY)],\n",
    "            app_context=app_context,\n",
    "            bundle_config_names=config_names,\n",
    "            bundle_path=model_path,\n",
    "            name=\"bundle_spleen_seg_op\",\n",
    "        )\n",
    "\n",
    "        # Create DICOM Seg writer providing the required segment description for each segment with\n",
    "        # the actual algorithm and the pertinent organ/tissue. The segment_label, algorithm_name,\n",
    "        # and algorithm_version are of DICOM VR LO type, limited to 64 chars.\n",
    "        # https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html\n",
    "        segment_descriptions = [\n",
    "            SegmentDescription(\n",
    "                segment_label=\"Spleen\",\n",
    "                segmented_property_category=codes.SCT.Organ,\n",
    "                segmented_property_type=codes.SCT.Spleen,\n",
    "                algorithm_name=\"volumetric (3D) segmentation of the spleen from CT image\",\n",
    "                algorithm_family=codes.DCM.ArtificialIntelligence,\n",
    "                algorithm_version=\"0.3.2\",\n",
    "            )\n",
    "        ]\n",
    "\n",
    "        custom_tags = {\"SeriesDescription\": \"AI generated Seg, not for clinical use.\"}\n",
    "\n",
    "        dicom_seg_writer = DICOMSegmentationWriterOperator(\n",
    "            self,\n",
    "            segment_descriptions=segment_descriptions,\n",
    "            custom_tags=custom_tags,\n",
    "            output_folder=app_output_path,\n",
    "            name=\"dicom_seg_writer\",\n",
    "        )\n",
    "\n",
    "        # Create the processing pipeline, by specifying the source and destination operators, and\n",
    "        # ensuring the output from the former matches the input of the latter, in both name and type.\n",
    "        self.add_flow(study_loader_op, series_selector_op, {(\"dicom_study_list\", \"dicom_study_list\")})\n",
    "        self.add_flow(\n",
    "            series_selector_op, series_to_vol_op, {(\"study_selected_series_list\", \"study_selected_series_list\")}\n",
    "        )\n",
    "        self.add_flow(series_to_vol_op, bundle_spleen_seg_op, {(\"image\", \"image\")})\n",
    "        # Note below the dicom_seg_writer requires two inputs, each coming from a source operator.\n",
    "        self.add_flow(\n",
    "            series_selector_op, dicom_seg_writer, {(\"study_selected_series_list\", \"study_selected_series_list\")}\n",
    "        )\n",
    "        self.add_flow(bundle_spleen_seg_op, dicom_seg_writer, {(\"pred\", \"seg_image\")})\n",
    "        # Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by\n",
    "        # uncommenting the following couple lines.\n",
    "        stl_conversion_op = STLConversionOperator(\n",
    "            self, output_file=app_output_path.joinpath(\"stl/spleen.stl\"), name=\"stl_conversion_op\"\n",
    "        )\n",
    "        self.add_flow(bundle_spleen_seg_op, stl_conversion_op, {(\"pred\", \"image\")})\n",
    "\n",
    "        # Create the ClaraViz operator and feed both the seg and input Image object to it\n",
    "        viz_op = ClaraVizOperator(self, name=\"clara_viz_op\")\n",
    "        self.add_flow(series_to_vol_op, viz_op, {(\"image\", \"image\")})\n",
    "        self.add_flow(bundle_spleen_seg_op, viz_op, {(\"pred\", \"seg_image\")})\n",
    "\n",
    "        logging.info(f\"End {self.compose.__name__}\")\n",
    "\n",
    "\n",
    "# This is a sample series selection rule in JSON, simply selecting CT series.\n",
    "# If the study has more than 1 CT series, then all of them will be selected.\n",
    "# Please see more detail in DICOMSeriesSelectorOperator.\n",
    "Sample_Rules_Text = \"\"\"\n",
    "{\n",
    "    \"selections\": [\n",
    "        {\n",
    "            \"name\": \"CT Series\",\n",
    "            \"conditions\": {\n",
    "                \"StudyDescription\": \"(.*?)\",\n",
    "                \"Modality\": \"(?i)CT\",\n",
    "                \"SeriesDescription\": \"(.*?)\"\n",
    "            }\n",
    "        }\n",
    "    ]\n",
    "}\n",
    "\"\"\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "if __name__ == \"__main__\":\n",
    "    AISpleenSegApp().run()\n",
    "```\n",
    "\n",
    "The above lines are needed to execute the application code by using `python` interpreter.\n",
    "\n",
    "### \\_\\_main\\_\\_.py\n",
    "\n",
    "\\_\\_main\\_\\_.py is needed for <a href=\"../../developing_with_sdk/packaging_app.html#required-arguments\">MONAI Application Packager</a> to detect the main application code (`app.py`) when the application is executed with the application folder path (e.g., `python simple_imaging_app`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing my_app/__main__.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile my_app/__main__.py\n",
    "from app import AISpleenSegApp\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    AISpleenSegApp().run()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "app.py\t__main__.py\n"
     ]
    }
   ],
   "source": [
    "!ls my_app"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time, let's execute the app in the command line.\n",
    "\n",
    ":::{note}\n",
    "Since the environment variables have been set and contain the correct paths, it is not necessary to provide the command line options on running the application.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[\u001b[32minfo\u001b[m] [fragment.cpp:599] Loading extensions from configs...\n",
      "[2025-01-29 14:42:26,898] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, argv=['my_app'])\n",
      "[2025-01-29 14:42:26,901] [INFO] (root) - AppContext object: AppContext(input_path=dcm, output_path=output, model_path=models, workdir=)\n",
      "[2025-01-29 14:42:26,904] [INFO] (root) - End compose\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:264] Creating context\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'input_folder'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'dicom_study_list'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'seg_image'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'output_file'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'output_folder'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'seg_image'\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2208] Activating Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2238] Running Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2240] Waiting for completion...\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:191] Scheduling 7 entities\n",
      "[2025-01-29 14:42:26,976] [INFO] (monai.deploy.operators.dicom_data_loader_operator.DICOMDataLoaderOperator) - No or invalid input path from the optional input port: None\n",
      "[2025-01-29 14:42:27,963] [INFO] (root) - Finding series for Selection named: CT Series\n",
      "[2025-01-29 14:42:27,963] [INFO] (root) - Searching study, : 1.3.6.1.4.1.14519.5.2.1.7085.2626.822645453932810382886582736291\n",
      "  # of series: 1\n",
      "[2025-01-29 14:42:27,963] [INFO] (root) - Working on series, instance UID: 1.3.6.1.4.1.14519.5.2.1.7085.2626.119403521930927333027265674239\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - On attribute: 'StudyDescription' to match value: '(.*?)'\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) -     Series attribute StudyDescription value: CT ABDOMEN W IV CONTRAST\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - On attribute: 'Modality' to match value: '(?i)CT'\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) -     Series attribute Modality value: CT\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - On attribute: 'SeriesDescription' to match value: '(.*?)'\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) -     Series attribute SeriesDescription value: ABD/PANC 3.0 B31f\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - Series attribute string value did not match. Try regEx.\n",
      "[2025-01-29 14:42:27,964] [INFO] (root) - Selected Series, UID: 1.3.6.1.4.1.14519.5.2.1.7085.2626.119403521930927333027265674239\n",
      "[2025-01-29 14:42:28,237] [INFO] (root) - Parsing from bundle_path: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models/model/model.ts\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/bundle/reference_resolver.py:216: UserWarning: Detected deprecated name 'optional_packages_version' in configuration file, replacing with 'required_packages_version'.\n",
      "  warnings.warn(\n",
      "Box(children=(Widget(), VBox(children=(interactive(children=(Dropdown(description='View mode', index=2, options=(('Cinematic', 'CINEMATIC'), ('Slice', 'SLICE'), ('Slice Segmentation', 'SLICE_SEGMENTATION')), value='SLICE_SEGMENTATION'), Output()), _dom_classes=('widget-interact',)), interactive(children=(Dropdown(description='Camera', options=('Top', 'Right', 'Front'), value='Top'), Output()), _dom_classes=('widget-interact',))))))\n",
      "[2025-01-29 14:42:39,484] [INFO] (monai.deploy.operators.stl_conversion_operator.STLConversionOperator) - Output will be saved in file output/stl/spleen.stl.\n",
      "[2025-01-29 14:42:41,141] [INFO] (monai.deploy.operators.stl_conversion_operator.SpatialImage) - 3D image\n",
      "[2025-01-29 14:42:41,141] [INFO] (monai.deploy.operators.stl_conversion_operator.STLConverter) - Image ndarray shape:(204, 512, 512)\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/highdicom/base.py:163: UserWarning: The string \"C3N-00198\" is unlikely to represent the intended person name since it contains only a single component. Construct a person name according to the format in described in https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#sect_6.2.1.2, or, in pydicom 2.2.0 or later, use the pydicom.valuerep.PersonName.from_named_components() method to construct the person name correctly. If a single-component name is really intended, add a trailing caret character to disambiguate the name.\n",
      "  check_person_name(patient_name)\n",
      "[2025-01-29 14:43:05,321] [INFO] (highdicom.base) - copy Image-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:43:05,322] [INFO] (highdicom.base) - copy attributes of module \"Specimen\"\n",
      "[2025-01-29 14:43:05,322] [INFO] (highdicom.base) - copy Patient-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:43:05,322] [INFO] (highdicom.base) - copy attributes of module \"Patient\"\n",
      "[2025-01-29 14:43:05,323] [INFO] (highdicom.base) - copy attributes of module \"Clinical Trial Subject\"\n",
      "[2025-01-29 14:43:05,323] [INFO] (highdicom.base) - copy Study-related attributes from dataset \"1.3.6.1.4.1.14519.5.2.1.7085.2626.936983343951485811186213470191\"\n",
      "[2025-01-29 14:43:05,323] [INFO] (highdicom.base) - copy attributes of module \"General Study\"\n",
      "[2025-01-29 14:43:05,323] [INFO] (highdicom.base) - copy attributes of module \"Patient Study\"\n",
      "[2025-01-29 14:43:05,323] [INFO] (highdicom.base) - copy attributes of module \"Clinical Trial Study\"\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:401] Scheduler finished.\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2243] Deactivating Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2251] Graph execution finished.\n",
      "[2025-01-29 14:43:05,475] [INFO] (app.AISpleenSegApp) - End run\n"
     ]
    }
   ],
   "source": [
    "!rm -rf $HOLOSCAN_OUTPUT_PATH\n",
    "!python my_app"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.2.826.0.1.3680043.10.511.3.78800183556923650623335517776550758.dcm  stl\n"
     ]
    }
   ],
   "source": [
    "!ls output"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Packaging app"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Clara-Viz operators present in an application are used for interactive visualization, so the application shall not be packaged."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
